<?php // Salesforce.php

class Salesforce {
	public static $cirruspath = false; // set to true if connecting to the cirruspath org
	public static $connection = array(
		"standard" => null,
		"cirruspath" => null
	);
	
	public static function connect() {
		$conn = null;
		if (self::$cirruspath) $conn = self::$connection["cirruspath"];
		else $conn = self::$connection["standard"];

		if (!$conn) {
			$conn = new SforcePartnerClient();
			$conn->createConnection(PARTNER_WSDL);
			if (self::$cirruspath) {
				$conn->setEndpoint('https://cirruspath.my.salesforce.com/services/Soap/u/22.0/00D30000000pgfe');
				$conn->login(CP_ORG_USERNAME, CP_ORG_PASSWORD.CP_ORG_SECTOKEN);
				self::$connection["cirruspath"] = $conn;				
			} else {
				$endpoint = str_replace('{version}', API_VERSION, $_SESSION['partner_endpoint']);
				$conn->setEndpoint($endpoint);
				$conn->setSessionHeader($_SESSION["access_token"]);
				$conn->setCallOptions(new CallOptions(SFDC_CLIENT_ID, null));
				self::$connection["standard"] = $conn;
			}
			
			$current_user = new InsightUser();
			$current_user->setInsightToken();
		}
		
		self::$cirruspath = false; // reset
		
		return $conn;
	}
	
	public static function insert($data) {
		$conn = self::connect();
		$records = (is_array($data) ? $data : array($data));
		foreach ($records as $rec) $rec->fields = Util::cleanFields($rec->type, $rec->fields, FALSE, TRUE);
		
		$result = null;
                ContactSyncUtility::debug('Records for inserts', $records);
		try {
			$result = $conn->create($records);
			if (is_array($result) && count($result) == 1) $result = $result[0];
		} catch (Exception $e) {
			$result = $e;
		}
		
		return $result;
	}

	public static function update($data) {
		$conn = self::connect();
		$records = (is_array($data) ? $data : array($data));
		
		$result = null;
		try {
			$result = $conn->update($records);
		} catch (Exception $e) {
			$result = $e;
		}
		
		return $result;
	}
	
	public static function search($type, $query) {
		$user = new InsightUser();
		$conn = Salesforce::connect();
		$results = array();
		
		// clean up search term
		$query = preg_replace('/[-&\]\[:]/i','',$query);
		
		$sosl = 'FIND {'.Util::escape($query).'} IN ALL FIELDS RETURNING ';
		if ($type == 'person') {
			$obj = array();
			if ($user->hasObj("Contact")) $obj[] = 'Contact (Id,Name,Account.Name ORDER BY LastModifiedDate DESC LIMIT 10)';
			if ($user->hasObj("Lead")) $obj[] = 'Lead (Id,Name,Company WHERE isConverted=false ORDER BY LastModifiedDate DESC LIMIT 10)';

			$sosl .= implode(",", $obj);
		} else {
			$sosl .= '  '.$type.' ('.implode(', ', $user->getAccessibleFields($type)).' LIMIT 10)';
		}
		
		$search = self::sfQueryProxy($conn, "search", $sosl);

		if (isset($search->searchRecords)) {
			foreach ($search->searchRecords as $rec) {
				$results[] = $rec;
			}
		}
		
		if ($type == 'person') {
			foreach ($results as $rec) {
				$rec->isLead = $rec->type=='Lead';
				$rec->isContact = $rec->type=='Contact';
			}
		}
		
		return $results;
	}
	
	// Pass one or more emails and return the corresponding objects
	public static function lookupEmails($emails) {
		if (!is_array($emails)) $emails = array($emails);
		$lookup = array();
		$user = new InsightUser();
		
		foreach ($emails as $e) {
			$email = Util::checkEmail($e);
			if ($email) $lookup[] = Util::escape(strtolower($email));
		}
		
		$leadRecTypeField = ''; $contactRecTypeField = '';
		if ($user->hasRecordTypes("Lead")) $leadRecTypeField = ', RecordTypeId';
		if ($user->hasRecordTypes("Contact")) $contactRecTypeField = ', RecordTypeId';
		
		// limit the search to only the first 15 email addresses
		$search = implode(" OR ", array_slice($lookup, 0, 15));
		$sosl = 'FIND {'.$search.'} IN EMAIL FIELDS RETURNING ';
				
		// Build a dynamic sosl query return set by looking for the visible email fields on the contact and lead objects
		$email_fields = array(); // an array of the email fields for Leads and Contacts
		foreach (array("Lead", "Contact") as $obj) {
			if ($user->hasObj($obj)) {
				$desc = $user->getObjDescribe($obj);
				$email_fields[$obj] = array();
				foreach ($desc->fields as $field) {
					if ($field->type == "email") $email_fields[$obj][] = $field->name;
				}
			}
		}
		
		$obj = array();
		if ($user->hasObj("Contact") && count($email_fields["Contact"])) {
			$contact_email_fields = implode(",", $email_fields["Contact"]);
			$obj[] = "Contact (Id, {$contact_email_fields}, LastModifiedDate {$contactRecTypeField} ORDER BY LastModifiedDate DESC)";
		}
		if ($user->hasObj("Lead") && count($email_fields["Lead"])) {
			$lead_email_fields = implode(",", $email_fields["Lead"]);
			$obj[] = "Lead (Id, {$lead_email_fields}, LastModifiedDate {$leadRecTypeField} WHERE isConverted=false ORDER BY LastModifiedDate DESC)";
		}

		$sosl .= implode(",", $obj);

		$conn = Salesforce::connect();
		$result = self::sfQueryProxy($conn, "search", $sosl);

		// resolve to a specific person
		$map = array();
		if (isset($result->searchRecords)) {
			foreach ($result->searchRecords as $sobj) {
				foreach (get_object_vars($sobj->fields) as $field => $value) {
					if (strlen($value) == 0) continue;
					if ($field == 'LastModifiedDate') continue;
					// only email fields left
					$email = strtolower($value);
					if (!array_key_exists($email, $map)) {
						// it's not mapped yet
						$map[$email] = $sobj;
					} else if ($sobj->type == 'Contact' && $map[$email]->type == 'Lead') {
						// it is mapped, but to a Lead and this is a Contact
						$map[$email] = $obj;
					} else if ($sobj->type == 'Contact' && $map[$email]->fields->LastModifiedDate < $sobj->fields->LastModifiedDate) {
						// it is mapped to a contact, but this one is more recent than the current
						$map[$email] = $sobj;
					}
				}
			}
		}
		
		// Pull from the new_people cache as well
		if (isset($_COOKIE["new_people"])) {
			$new_people = json_decode($_COOKIE["new_people"]);
			foreach ($emails as $email) {
				$email = strtolower($email);
				if (!isset($map[$email]) && isset($new_people->$email)) {
					$o = new SObject();
					$o->Id = $new_people->$email;
					$o->type = (stripos($o->Id, '00Q') === 0 ? 'Lead' : 'Contact');
					$o->fields = array("Email" => $email);
					$map[$email] = $o;
				}
			}
		}
 
		// ensure the returned result is in the same order as what was passed in
		$ordered = array();
		foreach ($emails as $email) {
			$email = strtolower($email);
			if (array_key_exists($email, $map)) {
				$ordered[$email] = $map[$email];
			}
		}

		return $ordered;
	}
	
	public static function getPerson($detail) {
		$user = new InsightUser();
		$conn = Salesforce::connect();
		$person = new SObject();
		
		$email = null; $personId = null;
		if (array_key_exists("email", $detail)) $email = $detail["email"];
		if (array_key_exists("id", $detail)) $personId = $detail["id"];
		
		// personId will either be passed in directly, or we can find it through matchPerson
		if (isset($email)) {
			$results = Salesforce::lookupEmails($email);
			if (isset($results[strtolower($email)])) $person = $results[strtolower($email)];
		} else {
			if (stripos($personId, '00Q') === 0) $person->type = 'Lead';
			else if (stripos($personId, '003') === 0) $person->type = 'Contact';
			$person->Id = $personId;
		}
		
		if (!isset($person) || !isset($person->Id) || $person->Id == "") {
			$person = new CIPerson(); // empty shell
			$person->getMatchingAccounts($detail["email"]);
			$person->Email = $detail["email"];

			// get 'edit' configs for leads and contacts
			$person->config = new stdClass();
			foreach (array("contact", "lead") as $obj) {
				if ($user->hasObj($obj)) {
					$c = array();
					foreach ($user->getObjConfig($obj) as $cfg) {
						if (isset($cfg->location) && ($cfg->location == "edit" || $cfg->location == "both")) {
							$c[] = $cfg;
						}
					}
					$person->config->$obj = $c;
				}
			}
		} else {
			// get the record type if it isn't already provided
			if ($user->hasRecordTypes($person->type) && !array_key_exists("RecordTypeId", get_object_vars($person->fields))) {
				$rec_type_query = "Select Id, RecordTypeId From {$person->type} Where Id='{$person->Id}'";
				$rt_result = Salesforce::sfQueryProxy($conn, "query", $rec_type_query);
				$rec = array_shift($rt_result->records);
				
				// sets null record types to the system 'global' RT
				if (!isset($rec->fields->RecordTypeId)) {
					$person->fields->RecordTypeId = '012000000000000AAA';
				} else {
					$person->fields->RecordTypeId = $rec->fields->RecordTypeId;
				}
			}
			
			$query = $user->getPersonQuery($person);
			
			try {
				Util::debug("getPerson query", $query);
				$result = self::sfQueryProxy($conn, "query", $query);
				Util::debug("getPerson result", $result);
				$person = new CIPerson(array_shift($result->records));
			} catch (Exception $e) {
				return $e;
			}
		}
		
		return $person;
	}

	public static function getMatchingAccounts($email) {
		$user = new InsightUser();
		if ($user->hasObj("Account") && $user->hasObj("Contact")) {
			$conn = Salesforce::connect();

			// Extract the domain from the email address
			$host = substr(strstr($email, '@'), 1);
			
			$queryFields = array("id", "name", "billingcity", "billingstreet", "billingstate", "billingcountry", "billingpostalcode", "phone");
			$accountFields = array();
			foreach ($user->getAccessibleFields("Account") as $f) {
				if (in_array(strtolower($f), $queryFields)) $accountFields[] = "Account.".$f;
			}
			
			$query = "Select ".implode(', ', $accountFields).", count(Id) ContactCount From Contact " .
				"Where Email LIKE '%@{$host}'";
			if ($user->isAccessible("account", "website")) 
				$query .= " OR Account.Website LIKE '%{$host}' ";
	
			$query .= "Group By " . implode(', ', $accountFields) . " Order By count(Id) desc LIMIT 15";
			
			$result = self::sfQueryProxy($conn, "query", $query);
			$accounts = array();
			if (isset($result->records)) {
				foreach ($result->records as $a) {
					$a->type = "Account";
					$accounts[] = $a;
				}
			}
			return $accounts;
		} else {
			return array();
		}
	}
	
	public static function logEmail($envelope) {
		$current_user = new InsightUser();
		$instanceUrl = $current_user->getInstanceUrl();

		// build description field
		$description = '';		
		if (isset($envelope->from->email)) $description .= 'From: ' . $envelope->from->email . "\n";
		if (isset($envelope->to)) $description .= 'Recipients: ' . Salesforce::formatEmailRecipients($envelope->to) . "\n";
					
		$description .= "\n"; // extra line break
		$description .= "Subject: " . (isset($envelope->subject) ? $envelope->subject : "") . "\n";
		$description .= "Body:\n" . substr($envelope->body,0,31000);
	
		$emails = array();
		if (isset($envelope->contextPerson->email)) $emails[] = $envelope->contextPerson->email;
		
		$task_relation = $current_user->hasObj("TaskRelation");
		if ($task_relation) {
			// TaskRelations are enabled, use multi-who
			foreach ($envelope->to as $r) $emails[] = $r->email;
		}
		if ($task_relation == false) {
			// double check
			$emails = array_slice($emails, 0, 1);
		} else {
			// uniqueify the list
			$emails = Util::array_iunique($emails);
			// make sure the user's email is not included
			$emails = Util::array_remove($emails, $current_user->email);
		}

		$emails = array_values(Salesforce::lookupEmails($emails));
		if (count($emails) == 0) {
			// No Lead/Contact found
			return CirrusInsight::ServiceResponse(false, CirrusInsight::ServiceError("RELATED_TO_NOT_FOUND"));
		}
		
		$who_ids = array();
		// If the first person is a lead, or if TaskRelation is not available
		// relate the email to the first person
		$who_ids[] = $emails[0]->Id;
		$whoid = $who_ids[0];
		// If the first person is a contact and TaskRelation IS available
		// queue up the other recipients to be added as well
		if ($emails[0]->type == "Contact" && $task_relation) {
			foreach ($emails as $sobj) {
				// remove leads
				if ($sobj->type == "Contact") $who_ids[] = $sobj->Id;
			}
		}
		
		$locale = new LocaleHelper();
		$date = $locale->parse($envelope->date);
		if ($date == null) $date = $envelope->date;

		$person = Salesforce::getPerson(array("id"=>$whoid));
		$taskObj = new SObject();
		$taskObj->type = 'Task';
		
		$fields = array(
			"ActivityDate" => $date,
			"Description" => $description,
			"OwnerId" => $current_user->user_id,
			"Status" => 'Completed',
			"Subject" => substr("Email: " . (isset($envelope->subject) ? $envelope->subject : ""), 0,255),
			"WhoId" => $whoid,
			"Type" => 'Email',
			"Priority" => 'Normal',
			"isTask" => true
		);
		if (isset($envelope->whatId) && $envelope->whatId != '') {
			$fields["WhatId"] = $envelope->whatId;
		} else if (stripos($whoid, '003') === 0) {
			// If they haven't chosen a whatid - and they're relating the email to a Contact
			// relate it to the Account as well.
			$fields["WhatId"] = $person->AccountId;
		}
		$taskObj->fields = $fields;
		
		// Insert/Update the task
		$result = Salesforce::insert($taskObj);
		
		// handle exceptions
		if (!isset($result->success) || !$result->success || get_class($result) == "SoapFault") {
			unset($taskObj->Description); // erase the email content
			Util::logCase('ERROR_LOG_EMAIL', array("EXCEPTION"=>Util::cleanSoapFaultException($result), "TASK OBJECT"=>$taskObj));
			return CirrusInsight::ServiceResponse(false, CirrusInsight::ServiceError("ERROR_TASK_UPSERT"));
		} else {
			// Task creation successful. Create Task Relations if configured
			if (count($who_ids)) {
				$task_relations = array();
				foreach ($who_ids as $id) {
					$trObj = new SObject();
					$trObj->type = 'TaskRelation';
					$trObj->fields = array(
						"TaskId" => $result->id,
						"RelationId" => $id
					);
					$task_relations[] = $trObj;
				}
				
				Salesforce::insert($task_relations);
			}
			
			$response = new stdClass();
			$response->taskId = $result->id;
			$response->who = $person;
			
			// For Opportunities and Contacts try to add a corresponding *ContactRole record
			if (isset($envelope->whatId) && $envelope->whatId != '' && stripos($whoid, '003') === 0) {
				$isOpp = stripos($envelope->whatId, '006') === 0;
				$isCase = stripos($envelope->whatId, '500') === 0;
				
				// Check to see if it's already in the list of returned records
				$addRole = true;
				if ($isOpp && isset($person->fields->Opportunities) && count($person->fields->Opportunities) > 0) {
					foreach ($person->fields->Opportunities as $opp) {
						if ($opp->Id == $envelope->whatId) {
							$addRole = false;
							break;
						}
					}
				}
				if ($isCase && isset($person->fields->Cases) && count($person->fields->Cases) > 0) {
					foreach ($person->fields->Cases as $case) {
						if ($case->Id == $envelope->whatId) {
							$addRole = false;
							break;
						}
					}
				}
				
				// The related record isn't one that was returned with the getPerson query
				// double check that another contact role doesn't already exist
				if ($addRole && ($isOpp || $isCase)) {
					$recField = ''; $objName = '';
					if ($isCase) {
						$recField = 'CasesId';
						$objName = 'Case';
					} else if ($isOpp) {
						$recField = 'OpportunityId';
						$objName = 'Opportunity';
					}
					
					$contact_role_query = "Select Id From {$objName}ContactRole Where ContactId = '{$person->Id}' AND {$recField} = '{$envelope->whatId}'";
					$conn = Salesforce::connect();
					$contact_role_res = Salesforce::sfQueryProxy($conn, 'query', $contact_role_query);
					
					if (!isset($contact_role_res) || !isset($contact_role_res->records) || count($contact_role_res->records) == 0) {
						$contact_role = new SObject();
						$contact_role->type = "{$objName}ContactRole";
						$contact_role->fields = array(
							"Role" => "{$objName} Contact",	
							"ContactId" => $person->Id,
							$recField => $envelope->whatId
						);
						
						$cr_result = Salesforce::insert($contact_role);
					}
				}
			}
			
			// Clear related cache entries
			// email addresses won't necessarily be known, so clear everything
			CICache::clearAll(); 
						
			return CirrusInsight::ServiceResponse(true, $response); // success
		}
	}
	
	public static function formatEmailRecipients($emailRecipients) {
		$s = '';
		foreach ($emailRecipients as $recipient) {
			$s .= $recipient->email . ', ';
		}
		$s = substr($s, 0, strlen($s)-2);
		return $s;
	}
	
	public static function getSfRecord($type, $recId) {
		$conn = Salesforce::connect();
		$user = new InsightUser();
		$query = "Select " . implode(', ', $user->getAccessibleFields($type)) . " From " . $type . " Where Id = '{$recId}'";
		$result = self::sfQueryProxy($conn, "query", $query);
		$record = array_pop($result->records);
		
		return $record;
	}
	
	public static function sfQueryProxy($conn, $queryType, $query) {
		$result = null;
		try {
			$result = $conn->$queryType($query);
		} catch (SoapFault $e) {
			$faultCode = '';
			if (isset($e->faultCode)) $faultCode = $e->faultCode;
			if (isset($e->faultcode)) $faultCode = $e->faultcode;
			if ($faultCode == 'sf:INVALID_SESSION_ID') {
				unset($conn);
				unset($_SESSION["access_token"]);
				Salesforce::$connection = null;
	
				$insightUser = new InsightUser();
				$login_result = $insightUser->requestAccessToken();
	
				if (!$login_result->success) {
					return $login_result;
				}
				
				// reconnect
				$conn = Salesforce::connect();
				try {
					$result = $conn->$queryType($query);
				} catch (SoapFault $e2) {
					Util::logCase($e2->getMessage(), array(
						"QUERY" => $query,
						"EXCEPTION1" => $e,
						"EXCEPTION2" => $e2
					));
				}
			} else if ($faultCode == 'HTTP') {
				// try again... one more time
				try {
					$result = $conn->$queryType($query);
				} catch (SoapFault $e3) {
					Util::logCase($e3->getMessage(), array(
						"QUERY" => $query,
						"EXCEPTION3" => $e3
					));
				} 
			} 
			
			if (!$result) {
				// another kind of exception that should have been handled earlier
				Util::logCase("QUERY_EXCEPTION", array("EXCEPTION"=>Util::cleanSoapFaultException($e), "QUERY"=>$query));
			}
		} catch (Exception $e) {
			Util::logCase($e->getMessage(), array(
				"QUERY" => $query,
				"EXCEPTION" => Util::cleanSoapFaultException($e)
			));
		}
		
		return $result;
	}
	
	public static function getAccountDetails($accountId) {
		$conn = Salesforce::connect();
		
		$query = "Select " . implode(', ', $user->getAccessibleFields("Account")) . ", " .
			"(Select " . implode(', ', $user->getAccessibleFields("Account")) . " From Contacts Order By LastName, Firstname) " .
			"From Account Where Id = '{$accountId}'";

		// TODO: try/catch
		$result = self::sfQueryProxy($conn, "query", $query);
		if ($result->size > 0) return $result->records[0];
		return false;
	}
	
	public static function getUserInfo($userId) {
		$conn = Salesforce::connect();
		$ciuser = new InsightUser($userId);
		$fields = implode(', ', $ciuser->getAccessibleFields("User"));
		if ($ciuser->hasObj("profile")) $fields .= "Profile.Name, Profile.PermissionsCustomizeApplication";
		$query = "Select ".$fields." From User Where Id = '{$userId}'";
		
		$user = self::sfQueryProxy($conn, "query", $query);
		$user = array_shift($user->records);
		return $user;
	}
	
	// Returns general org info and user count
	public static function getOrgInfo() {
		$user = new InsightUser();
		
		$conn = Salesforce::connect();
		$result = new stdClass();
			$result->org = null;
			$result->numUsers = 0;
		
		if ($user->hasObj("Organization")) {
			$org_query = "Select ".implode(', ', $user->getAccessibleFields("Organization"))." From Organization";
			$org = self::sfQueryProxy($conn, "query", $org_query);
			$org = array_shift($org->records);
			$result->org = $org;
		}

		$active_users_query = "Select Id From User Where isActive=true AND UserType = 'Standard'";
		$active_users = self::sfQueryProxy($conn, "query", $active_users_query);
		$result->numUsers = $active_users->size;
		
		return $result;
	}
	
	public static function mapObjFields($type, $data) {
		$user = new InsightUser();

		$fields = $user->getAccessibleFields($type);
		$fieldList = array();
		foreach ($fields as $f) {
			if (isset($data->$f) && $f != 'Id') {
				$fieldList[$f] = $data->$f;
			}
		}
		
		return $fieldList;
	}
}

?>